package com.ipan.kits.text;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.xml.sax.InputSource;

/**
 * XML、Map互转工具
 * 
 * 针对部分xml的格式是动态变化的，不是固定格式，这种不太适合使用XML与JavaBean的映射，所以，改为使用XML与Map的映射；
 * 
 * 注意：不支持带属性的XML，适用于键值对的XML；
 * 
 * @author iPan
 * @version 2019-03-21
 */
public class Dom4jXmlMapUtil {
	
	private static final String ROOT_ELEMENT_NAME = "xml";
	private static final String ENCODING = "utf-8";
	private static final boolean IS_FORMAT = true;
	private static final boolean IS_CDATA = false; // true 说明不被XML解析器解析
	
	/**
	 * Map转XML，支持Map可以多层转换；
	 */
	public static String mapToXml(String rootName, Map<String, ?> params, boolean isCDATA, boolean isFormat) {
		Document doc = DocumentHelper.createDocument();
		doc.addElement(rootName);
		iteratorXml(doc.getRootElement(), params, isCDATA);
		return isFormat ? formatXML(doc.asXML()) : doc.asXML();
	}
	public static String mapToXml(String rootName, Map<String, ?> params) {
		return mapToXml(rootName, params, IS_CDATA, IS_FORMAT);
	}
	public static String mapToXml(Map<String, ?> params) {
		return mapToXml(ROOT_ELEMENT_NAME, params, IS_CDATA, IS_FORMAT);
	}
	
	/**
	 * 循环遍历创建xml节点
	 */
	@SuppressWarnings("unchecked")
	public static void iteratorXml(Element element, Map<String, ?> params, boolean isCDATA) {
		Set<String> set = params.keySet();
		for (Iterator<String> it = set.iterator(); it.hasNext();) {
			String key = (String) it.next();
			Object objVal = params.get(key);
			if (objVal instanceof Map) {
				Element e = element.addElement(key);
				iteratorXml(e, (Map<String, Object>) objVal, isCDATA);
			} else {
				String value = (objVal == null) ? "" : objVal.toString();
				if (isCDATA) {
					if (objVal instanceof Number) {
						element.addElement(key).addText(value);
					} else {
						element.addElement(key).addCDATA(value);
					}
				} else {
					element.addElement(key).addText(value);
				}
			}
		}
	}

	/**
	 * 格式化XML
	 */
	public static String formatXML(String inputXML) {
		String requestXML = null;
		XMLWriter writer = null;
		Document document = null;
		try {
			SAXReader reader = new SAXReader();
			document = reader.read(new StringReader(inputXML));
			if (document != null) {
				StringWriter stringWriter = new StringWriter();
				OutputFormat format = new OutputFormat("	", true);// 格式化，每一级前的空格
				format.setNewLineAfterDeclaration(false); // xml声明与内容是否添加空行
				format.setSuppressDeclaration(false); // 是否设置xml声明头部
				format.setNewlines(true); // 设置分行
				writer = new XMLWriter(stringWriter, format);
				writer.write(document);
				writer.flush();
				requestXML = stringWriter.getBuffer().toString();
			}
			return requestXML;
		} catch (Exception e1) {
			e1.printStackTrace();
			return null;
		} finally {
			if (writer != null) {
				try {
					writer.close();
				} catch (IOException e) {
				}
			}
		}
	}

	/**
	 * XML转Map
	 */
	public static Map<String, Object> xmlToMap(String xml) {
		return xmlToMap(xml, false);
	}
	public static Map<String, Object> xmlToMap(String xml, boolean hasRoot) {
		Document doc = null;
		try {
			doc = parseText(xml);
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (doc == null) {
			return null;
		}
		Map<String, Object> eleMap = new HashMap<String, Object>();
		Map<String, Object> retMap = eleMap;
		Element rootElement = doc.getRootElement();
		elementToMap(rootElement, eleMap);
		if (hasRoot) {
			String ename = rootElement.getName();
			retMap = new HashMap<String, Object>();
			retMap.put(ename, eleMap);
		}
		return retMap;
	}

	/***
	 * 递归遍历Element，放入Map；
	 */
	@SuppressWarnings("unchecked")
	public static void elementToMap(Element outEle, Map<String, Object> outMap) {
		List<Element> list = outEle.elements();
		if (list.size() < 1) { // elements()返回不会为null
			return ;
		}
		for (Element e : list) {
			if (e.elements().size() < 1) { // 不存在子元素
				outMap.put(e.getName(), e.getTextTrim());
			} else { // 存在子元素
				String ename = e.getName();
				Map<String, Object> innerMap = new HashMap<String, Object>();
				outMap.put(ename, innerMap);
				elementToMap(e, innerMap);
			}
		}
	}
	
	private static String getEncoding(String text) {
        String result = null;
        String xml = text.trim();
        if (xml.startsWith("<?xml")) {
            int end = xml.indexOf("?>");
            String sub = xml.substring(0, end);
            StringTokenizer tokens = new StringTokenizer(sub, " =\"\'");

            while (tokens.hasMoreTokens()) {
                String token = tokens.nextToken();

                if ("encoding".equals(token)) {
                    if (tokens.hasMoreTokens()) {
                        result = tokens.nextToken();
                    }
                    break;
                }
            }
        }
        return (result != null && result.length() > 0) ? result : ENCODING;
    }
	
	public static Document parseText(String xml) throws Exception {
		Document doc = null;
		SAXReader reader = new SAXReader();
		reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // XXE漏洞防御
//		reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
//		reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
//		reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
		String encoding = getEncoding(xml);
        InputSource source = new InputSource(new StringReader(xml));
        source.setEncoding(encoding);
        doc = reader.read(source);

        // if the XML parser doesn't provide a way to retrieve the encoding,
        // specify it manually
        if (doc.getXMLEncoding() == null) {
            doc.setXMLEncoding(ENCODING);
        }
        return doc;
	}

//	public static void main(String[] args) {
//		Map<String, Object> data = new HashMap<String, Object>();
//		data.put("request", "getData");
//		Map<String, Object> map = new HashMap<String, Object>();
//		map.put("date", "2019-03-21");
//		map.put("name", "张三");
//		map.put("age", 11);
//		data.put("data", map);
//		
//		System.out.println(mapToXml(data));
//		System.out.println(mapToXml("root", data));
//		System.out.println(mapToXml("root", data, true, true));
//		
//		System.out.println(xmlToMap(mapToXml(data)));
//		System.out.println(xmlToMap(mapToXml(data), true));
//	}

}
